  //constant unit... endTime/TUNIT is the default timeline length (otherwise it would be waaaay long)
  //alternately, I could just set the initial zoom to be 1/15... but starting with default 1.0 seems better
  int TUNIT = 15;
  
  
/* This tab contains the Timeline Layer, the "Track" class, and the Timeline Property Viewer Layer */

class Timeline extends Layer
{
  ArrayList data;  //holds our dataInstances
  String saveDir;  //where did we load it from?
  int numTracks = 1;  
  int startTime = 0;  //milliseconds, can't be changed
  int endTime = 30000;
  float hScale = 1.0;  //horizontal scale
  float vScale = 25;  //vertical scale
  int[] tIconOrder = {2,7,8,6,9,10};
  
  PGraphics display;
  PGraphics timeline;
  float[] offset = {0,0};
  
  int selected;
  int hSelected;
  
  float unit;
  float spacing;
  
  int over;
  int hOver;
  
  int displayWidth;
  int displayHeight;
  
  //hSlider variables
  boolean   hSliderNeeded = true;
  boolean   invertX = false;
  boolean   hSliderDrag = false;
  float     hSliderMin = 40;
  float     hSliderMax = 160;
  float     hSliderWidth = 30;
  float     hSliderX = hSliderMin;
  float     hSlideAmt = 0;
  
  //vSlider variables
  boolean   sliderNeeded = true;
  boolean   invertY = false;
  boolean   sliderDrag = false;
  float     sliderMin = 10;
  float     sliderMax = 166;
  float     sliderHeight = 30;
  float     sliderY = sliderMax;  //start at the bottom and scroll up if neccessary
  float     slideAmt = 0;
  
  dataInstance DRAGINSTANCE = null;
  dataInstance mouseOver = null;
  dataInstance SELECTED = null;
  
  boolean DODRAG = false;
  int requestTime;
  int lastClick = millis();
 
  Timeline()
  {
    super(43,250,757,230,0);
    MINWIDTH = 300;
    MINHEIGHT = 166;
    RESIZEENABLED = true;
    data = new ArrayList();
    for(int i = 0; i < numTracks; i++)
    {
      data.add(new Track(endTime));
    }
    saveDir = "projects";  //for ease ATM, going to limit saving to the /projects folder
    display =  createGraphics(w,h,JAVA2D);
    drawTimeline();
  }
  
  boolean mDraw()
  {
    pg = createGraphics(w,h,JAVA2D);
    pg.beginDraw();
    pg.background(BORDERCOLOR);
    pg.fill(FILLCOLOR);
    pg.noStroke();
    pg.rect(3,3,pg.width-6,pg.height-6);
    
    float useW = w;
    float useH = h;
    if(sliderNeeded) useW -= 28 - 25;
    if(hSliderNeeded) useH -= 16;
    
    sliderMax = useH - sliderHeight - 10;
    hSliderMax = useW - hSliderWidth - 10;
    
    hSliderX = constrain(hSliderX,hSliderMin,hSliderMax);
    sliderY = constrain(sliderY,sliderMin,sliderMax);
    
    float hUsable = hSliderMax-hSliderMin;
    float hPct = (hSliderX-hSliderMin)/hUsable;
    float hSlideTotal = max(0,displayWidth - hUsable - hSliderWidth);
    hSlideAmt = hSlideTotal*hPct;
    hOver = (int)((mouseX-x-15-25 + hSlideAmt) * unit/spacing);

    float usable = sliderMax-sliderMin;
    float pct = 1.0 - (sliderY-sliderMin)/usable;   //invert it
    float slideTotal = max(0,displayHeight - (h - (hSliderNeeded?66:50)));
    slideAmt = slideTotal*pct;
    over = (int)(((y+h+slideAmt-(hSliderNeeded?56:40)) - mouseY)/(vScale+2));

    drawDisplay(10-hSlideAmt+25,h-30-(hSliderNeeded?26:10) - displayHeight + slideAmt);
    pg.image(timeline,10-hSlideAmt+25,h-(hSliderNeeded?56:40));
    
    //now draw the borders back!
    pg.noStroke();
    pg.fill(FILLCOLOR);
    pg.rect(0,0,w,10); pg.rect(0,h-(hSliderNeeded?30:10),w,(hSliderNeeded?30:10));
    pg.rect(0,0,35,h); pg.rect(w-(sliderNeeded?30:10),0,(sliderNeeded?30:10),h);
    pg.fill(BORDERCOLOR);
    pg.rect(0,0,w,3); pg.rect(0,h-3,w,3);
    pg.rect(0,0,3,h); pg.rect(w-3,0,3,h);
    
    sliderMax = useH - sliderHeight - 10;
        
    //draw the slider if need-be
    if(sliderNeeded)
    {
      if(sliderDrag) sliderY -= (invertY?-1:1)*(pmouseY-mouseY);//sliderY = constrain(mouseY-y-sliderHeight/2,sliderMin,sliderMax);
      sliderY = constrain(sliderY,sliderMin,sliderMax);
      pg.strokeWeight(1);
      pg.stroke(BUTTONSTROKE);
      pg.fill(BUTTONSTROKE);
      pg.rect(w-22,sliderMin,6,sliderMax-sliderMin+sliderHeight);
      pg.fill(BUTTONFILL);
      pg.rect(w-28,sliderY,18,sliderHeight);
    }
    
    //draw the slider if need-be
    if(hSliderNeeded)
    {
      if(hSliderDrag) hSliderX -= (invertX?-1:1)*(pmouseX-mouseX); //constrain(mouseX-x-hSliderWidth/2,hSliderMin,hSliderMax);
      hSliderX = constrain(hSliderX,hSliderMin,hSliderMax);
      pg.strokeWeight(1);
      pg.stroke(BUTTONSTROKE);
      pg.fill(BUTTONSTROKE);
      pg.rect(hSliderMin,h-22,hSliderMax-hSliderMin+hSliderWidth,6);
      pg.fill(BUTTONFILL);
      pg.rect(hSliderX,h-28,hSliderWidth,18);
    }

    drawUI();    
    pg.endDraw();
    pg.modified = true;
    
    doCursor();
    
    return DONE;
  }
  

  //If I have time, it would be nice to make the data autofit to the currently sized width and then
  //from there allow the user to scale in/out
  void drawDisplay(float xOff, float yOff)
  {
    pg.pushMatrix();
    pg.translate(xOff,yOff);
    float widthNeeded = endTime/TUNIT*hScale;
    float heightNeeded = (vScale+2)*numTracks;
    
    float useW = w - 20 - 25;
    float useH = h - 50;
    
    if(heightNeeded > useH) sliderNeeded = true;
    else sliderNeeded = false;
    
    if(widthNeeded > useW) hSliderNeeded = true;
    else hSliderNeeded = false;
    
    if(sliderNeeded) useW -= 20;
    if(hSliderNeeded) useH -= 20;

    displayWidth = (int)max(useW,widthNeeded+10);
    displayHeight = (int)max(useH,heightNeeded);
    
    mouseOver = null;
    //next draw any currently added instances in each Track    
    pg.fill(color(24,50,90));
    pg.rect(0,0,displayWidth,displayHeight);
    float fontSize = min(12,vScale-1);
    float fontOffset = vScale/2.0 + fontSize/2.0;
    pg.textFont(font,fontSize);
    pg.textAlign(LEFT);
    for(int i = 0; i < numTracks; i++)
    {
      pg.stroke(37,74,135);
      pg.fill(114,159,207);
      pg.rect(5,displayHeight-vScale - (vScale+2)*i,widthNeeded,vScale);
      Track track = (Track)data.get(i);
      for(int j = 0; j < track.size(); j++)
      {
        pg.fill(37,74,135);
        pg.noStroke();        
        dataInstance di = (dataInstance)track.get(j);
        //keep a record of which instance the mouse is over so we don't have to sort through them
        //again on all the mouseEvents
        if(di.contains(hOver) && !DODRAG && i == over) {
          pg.stroke(255); 
          mouseOver = di;
        }
        if(di == SELECTED)
        { 
          pg.fill(52,101,164);
          pg.stroke(255);
        }
        float len = (float)di.getDuration() * spacing/unit; 
        float dx = 5+(float)di.offset * spacing/unit;
        float dy = displayHeight-vScale - (vScale+2)*i;
        pg.rect((int)dx,(int)dy,(int)len,vScale);
        pg.fill(255);
        if(pg.textWidth(di.name) + 2 < len)
        {
          if(pg.textWidth(di.name) + 6 < len) pg.text(di.name,dx+3,dy+fontOffset);
          else pg.text(di.name,dx+1,dy+fontOffset);
        }else if(textWidth("...") + 2 > len) {
          //do nothing...it's too small for a label!
        }else{
          String disp = di.name;
          while(textWidth(disp + "...") + 2 > len)
          {
            if(disp.length() == 1){ disp = ""; break;}
            disp = disp.substring(0,disp.length()-1);
          }
          pg.text(disp,dx+1,dy+fontOffset);
        }
      }
    }

    //finally draw any currently-being-dragged data instances
    if(DODRAG && DRAGINSTANCE != null)
    {
      float len = (float)DRAGINSTANCE.getDuration() * spacing/unit;     
      DRAGINSTANCE.offset = hOver - (DRAGINSTANCE.clickPct*DRAGINSTANCE.getDuration());
      if(DRAGINSTANCE.original == null || DRAGINSTANCE.original.offset != DRAGINSTANCE.offset)
      {
        pg.noStroke();
        if(testOverlap()) pg.fill(204,0,0);
        else pg.fill(0,204,0);
    
        //float len = (float)DRAGINSTANCE.getDuration() * spacing/unit;
        float dx = 5+(float)DRAGINSTANCE.offset * spacing/unit;
        float dy = displayHeight-vScale - (vScale+2)*over;
        //println(DRAGINSTANCE.offset + " " + dx + " " + dy + " " + len);
        pg.rect((int)dx,(int)dy,(int)len,vScale);
      }
    }    

    //draw the updated timeline
    pg.popMatrix();
    drawTimeline();
  }
  
  void mKeyUp()
  {
   if(SELECTED == null) return;
   //I should really check to see if this causes overlap problems...
   if(keyCode == LEFT)  SELECTED.offset--;
   if(keyCode == RIGHT) SELECTED.offset++;
   if(keyCode == DELETE){
     Track track = (Track)data.get(getTrackNum(SELECTED));
     track.remove(SELECTED);
     SELECTED = null;
   }

  }
  
  void purge(dataFile d)
  {
    for(int i = 0; i < numTracks; i++)
    {
      Track track = (Track)data.get(i);
      for(int j = 0; j < track.size(); j++)
      {
        dataInstance di = (dataInstance)track.get(j);
        if(di.file.equals(d))
        {
          track.remove(di);
          j--;
        }
      }
    }
  }
  
  void autoFit()
  {
    float widthNeeded = endTime/TUNIT;
    float useW = w - 20 - 25;
    
    if(widthNeeded > useW) hSliderNeeded = true;
    else hSliderNeeded = false;
    
    if(sliderNeeded) useW -= 20;
    
    hScale = useW/widthNeeded;    
  }
  
  void drawTimeline()
  {
    float widthNeeded = endTime/TUNIT*hScale;
    float useW = w - 20 - 25;
    
    if(widthNeeded > useW) hSliderNeeded = true;
    else hSliderNeeded = false;
    
    if(sliderNeeded) useW -= 20;
    
    timeline =  createGraphics((int)max(useW,widthNeeded+10),30,JAVA2D);
    timeline.beginDraw();
    timeline.background(color(24,50,90));
    
    //figure out what unit of measure is appropriate...
    float displayAmt = useW / (displayWidth - 10) * endTime;
    
    int[] unitList = {10,20,50};
    int whichUnit = 0;
    while((int)displayAmt / unitList[whichUnit] > 10)
    {
      whichUnit++;
      if(whichUnit == 3)
      {
        whichUnit = 0;
        for(int i = 0; i < 3; i++)
        {
          unitList[i] *= 10;
        }
      }
    }

    unit = unitList[whichUnit];
    int numUnits = ((int)endTime /  (int)unit);
    spacing = (int)widthNeeded / numUnits;
    timeline.textFont(font,12);
    timeline.textAlign(CENTER);
    timeline.stroke(114,159,207);
    for(int i = 0; i < endTime / unit + 1; i++)
    {
      if(i*unit > endTime) break;
      timeline.text("" + (int)(i*unit),5 + spacing*i,timeline.height-5);
      for(float j = 0;j < 10; j++)
      {
        timeline.line(5+spacing*i+(float)spacing*j/10.0,timeline.height-25,5+spacing*i+(float)spacing*j/10.0,timeline.height-(j%5 == 0?17:21));
        if(i*unit + j/10.0*(float)unit  >= endTime) break;
      }
    }
    
    timeline.endDraw();
    timeline.modified = true;    
  }
  
  void mPress()
  {    
    if(UIPress()) return;
    
    float useW = w-20-25;
    float useH = h-20;    
    if(sliderNeeded) useW -= 28;
    if(hSliderNeeded) useH -= 16;
    
    sliderMax = useH - sliderHeight;
    float usable = sliderMax-sliderMin;
    float pct = (sliderY-sliderMin)/usable;
    float slideTotal = max(0,useH - 10 - sliderMax);
    slideAmt = slideTotal*pct;
    
    hSliderMax = useW - hSliderWidth - 10;
    float hUsable = hSliderMax-hSliderMin;
    float hPct = (hSliderX-hSliderMin)/hUsable;
    float hSlideTotal = max(0,useW - 10 - hSliderMax);
    hSlideAmt = hSlideTotal*hPct;  
    
    if(sliderNeeded && mouseX > x + w - 28 && mouseX < x + w - 10 && mouseY > y+sliderY && mouseY < y+sliderY + sliderHeight){
      sliderDrag = true;
    }else if(hSliderNeeded && mouseX > x + hSliderX && mouseY > y + h-28 && mouseX < x + hSliderX + hSliderWidth && mouseY < y + h -10){
      hSliderDrag = true;
    }else if(mouseX > x + 35 && mouseX < x + useW && mouseY > y+10 && mouseY < y+useH-30){
      if(mouseOver == null)
      {
        sliderDrag = true;
        hSliderDrag = true;
        invertX = true;
        invertY = true;
      }
    }else{
      dragActive = true;
      if(RESIZEENABLED) checkResize();     
    }
  }
  
  void mDrag()
  {
    super.mDrag();
    if(invertX || invertY || dragActive || resizeActive[0] || resizeActive[1] || resizeActive[2] || resizeActive[3]) return;
    float useW = w-20-25;
    float useH = h-20;    
    if(sliderNeeded) useW -= 28;
    if(hSliderNeeded) useH -= 16;    
    
    if(!DODRAG && mouseX > x + 35 && mouseX < x + useW && mouseY > y+10 && mouseY < y+useH-30){
      if(mouseOver != null)
      {
        DODRAG = true;
        DRAGINSTANCE = mouseOver.copy(hOver);
      }
    }
  }

  void mRelease()
  {
    if(DODRAG && DRAGINSTANCE != null)
    {
      freezeDrag();
    }

    if(UIRelease()) return;
    
    if(millis() - lastClick < DOUBLECLICK && mouseOver != null)
    {
      messages.add(new Message(true,ACTIVELAYER,13));
    }
    lastClick = millis();
    
    if(mouseOver != null) SELECTED = mouseOver;
    
    sliderDrag = false;
    hSliderDrag = false;
    invertX = false;
    invertY = false;
  }

  void drawUI()
  {
    pg.fill(BORDERCOLOR);
    //pg.rect(0,0,35,3);
    pg.rect(33,0,2,2+tIconOrder.length*31);
    pg.rect(0,2+tIconOrder.length*31,35,3);

    pg.strokeWeight(1);
    
    for(int i = 0; i < tIconOrder.length; i++)
    {
      pg.stroke(BUTTONSTROKE);
      if(mouseX > x+3 && mouseX < x+35 && mouseY > y+3+31*i && mouseY < y+35+31*i)
      {
        if(mousePressed) pg.fill(BUTTONDOWN);
        else pg.fill(BUTTONOVER);
      }
      else{
        pg.fill(BUTTONFILL);
      }
      pg.rect(3,3+31*i,31,31);   
      pg.image(icons[tIconOrder[i]],6,7+31*i,24,24);
    }
  }

  boolean UIPress()
  {
    if(mouseX < x+35) return true;
    return false;
  }

  boolean UIRelease()
  {
    boolean rTrue = false;
    if(mouseX > x+3 && mouseX < x+35 && mouseY > y+12 && mouseY < y+12+tIconOrder.length*31){
      int whichButton = tIconOrder[(int)((mouseY-y-12)/31)];

      switch(whichButton)
      {
        case 7:
          zoomIn();
          rTrue = true;
          break;
        case 8:
          zoomOut();
          rTrue = true;
          break;
        case 2:
          messages.add(new Message(true,ACTIVELAYER,4));
          rTrue = true;
          break;
        case 6:
          data.add(new Track(endTime));
          numTracks++;
          break;
        case 9:
          cullEmptyTracks();
          if(numTracks == 0) 
          {
            numTracks++;
            data.add(new Track(endTime));
          }
          break;
        case 10:
          autoFit();
          break;
        default:
          println("Unrecognized icon!");
          break;
      }
    }
    return rTrue;
  }
  
  void cullEmptyTracks()
  {
    for(int i = 0; i < numTracks;  i++)
    {
      Track track = (Track)data.get(i);
      if(track.size() == 0) 
      {
        data.remove(i);
        numTracks--;
        i--;
      }
    }
  }
  
  void zoomIn()
  {
    hScale *= 2;
  }
  
  void zoomOut()
  {
    hScale /=2;
  }
  
  void setName(String s)
  {
    PROJECTNAME = s;
  }
  
  void setDir(String s)
  {
    saveDir = s;
  }
  
  boolean testOverlap()
  {
    if(!DODRAG || DRAGINSTANCE == null || over + 1 > data.size() || over < 0) return false;
    //figure out where the dragFile would get dropped... (should be moved to mDraw)
    DRAGINSTANCE.offset = hOver - (DRAGINSTANCE.getDuration() * DRAGINSTANCE.clickPct);
    Track track = (Track)data.get(over);
    for(int i = 0; i < track.size(); i++)
    {
      dataInstance d = (dataInstance)track.get(i);
      if(DRAGINSTANCE.original == d) continue;
      if(d.mOverlaps(DRAGINSTANCE)) return true;
    }
    return false;    
  }
  
  void softSetEndTime(int t)
  {
    boolean willClip = false;
    for(int i = 0; i < data.size(); i++)
    {
      Track track = (Track)data.get(i);
      if(track.willClip(t))
      {
        willClip = true;
        break;
      }
    }
    if(willClip)
    {
      requestTime = t;
      layers.add(new PopConfirm(ACTIVELAYER, 11, "Clipping will occur.  Continue?", 0));
      ACTIVELAYER++;
    }else{
      setEndTime(t);
    }
    
  }
  
  void setEndTime(int t)
  {
    endTime = t;
    for(int i = 0; i < data.size(); i++)
    {
      Track track = (Track)data.get(i);
      track.setEndTime(t);
    }
  }
  
  //for now, just disallow overlapping placements...in the future, allow for clipping adjustments
  void freezeDrag()
  {
    if(DRAGINSTANCE == null || DODRAG == false) return;
    
    if(DRAGINSTANCE.offset < 0 || DRAGINSTANCE.offset + DRAGINSTANCE.getDuration() > endTime ||
        over < 0 || over > numTracks-1)
    {
        DODRAG = false;
        DRAGINSTANCE = null;
        return;
    }
    
    Track track = (Track)data.get(over);
    if(testOverlap()) {
      DODRAG = false;
      DRAGINSTANCE = null;
      return;
    }else{
      //go through and find the old one
      for(int i = 0; i < numTracks; i++)
      {
        Track t = (Track)data.get(i);
        if(t.contains(DRAGINSTANCE.original))
        {
          t.remove(DRAGINSTANCE.original);
          break;
        }
      }
      //add the new data...
      if(DRAGINSTANCE.original != null)
      {
        track.add(DRAGINSTANCE.original);
        //make it match...
        DRAGINSTANCE.original.conform(DRAGINSTANCE);
      }else{
        track.add(DRAGINSTANCE);
      }
      
      DODRAG = false;
      DRAGINSTANCE = null;
    }
  }
  
  //cycles through the various tracks and finds which track the given dataInstance resides in
  int getTrackNum(dataInstance di)
  {
    for(int i = 0; i < data.size(); i++)
    {
      Track track = (Track)data.get(i);
      if(track.contains(di)) return i;
    }
    return -1;
  }
  //tests to see if a hypothetical dataInstance on a given track would overlap anything
  boolean testClear(dataInstance di, Track t)
  {
      boolean canChange = true;
      for(int i = 0; i < t.size(); i++)
      {
        dataInstance d = (dataInstance)t.get(i);
        if(di.original == d) continue;
        if(d.mOverlaps(di)) return false;
      }
      println("Made it!");
      return true; 
  }
}

class Track extends ArrayList
{
  int endTime;
  //ArrayList data;

  Track(int t)
  {
    super();
    endTime = t;
    //data = new ArrayList();
  }
  
  //basically tests to see if calling setEndTime(t) will clip data
  boolean willClip(int t)
  {
    for(int i = 0; i < size(); i++)
    {
      dataInstance d = (dataInstance)get(i);
      int len = d.outPoint - d.inPoint;
      if(d.offset > t) return true;
      if(d.offset + len > t) return true;
    }
    return false;
  }
  
  void setEndTime(int t)
  {
    endTime = t;
    for(int i = 0; i < size(); i++)
    {
      dataInstance d = (dataInstance)get(i);
      int len = d.outPoint - d.inPoint;
      if(d.offset > endTime)
      {
        remove(i);
        i--;
      }else if(d.offset + len > endTime){
        d.outPoint -= (d.offset+len - endTime);
      }
    }
  } 
}

class timelineProp extends Layer   
{
  Timeline t;
  int lastClick = 0;
  
  timelineProp(Timeline _t, int _x, int _y, int _w, int _h)
  {
    super(_x,_y,_w,_h,0); 
    RESIZEENABLED = true;
    MINWIDTH = 190;
    MINHEIGHT = 170;
    t = _t;
  }
  
  boolean mDraw()
  {
    pg = createGraphics(w,h,JAVA2D);
    pg.beginDraw();
    pg.background(BORDERCOLOR);
    pg.fill(FILLCOLOR);
    pg.noStroke();
    pg.rect(3,3,pg.width-6,pg.height-6);
    pg.fill(TEXTCOLOR);
    pg.textFont(font,20);
    pg.text("Timeline Properties",10,26);
    pg.fill(BORDERCOLOR);
    pg.rect(10,30,w-20,h-40);
    pg.fill(FILLCOLOR);
    pg.rect(12,32,w-24,h-44);
  
    pg.fill(TEXTCOLOR);
    pg.textFont(font,14);
    pg.textAlign(LEFT);
    pg.text("Name:",14,48);
    pg.text("Save Dir:",14,64);
    pg.text("Length:",14,80);
    pg.text("NumTracks:",14,96);
    pg.text("Vertical Scale:",14,112);
    //TODO: add an amalgamated count for DataPoints, numDigital, numAnalog
    //-track how many instances are "live" on the timeline

    pg.textAlign(RIGHT);
    pg.text(PROJECTNAME,w-14,48);
    pg.text(t.saveDir,w-14,64);
    pg.text(t.endTime,w-14,80);
    pg.text(t.numTracks,w-14,96);
    pg.text((int)t.vScale,w-14,112);
    pg.endDraw();    
    
    doCursor();
    
    return DONE;    
  }
  
  void mRelease()
  {
    if(mouseX > x+14 || mouseX < x+w-14)
    {
        int whichClick = (int)((mouseY-y-32)/16);
        println(whichClick);
        if(whichClick == 0)
        {
          layers.add(new PopText(ACTIVELAYER,5,"Rename project:",0));
          ACTIVELAYER++;
        }
        if(whichClick == 1)
        {
          layers.add(new PopText(ACTIVELAYER,6,"Save directory:",0));
          ACTIVELAYER++;
        }        
        if(whichClick == 2)
        {
          layers.add(new PopText(ACTIVELAYER,7,"Enter new length:",0));
          ACTIVELAYER++;
        }
        if(whichClick == 3)
        {
          popWarn("Use the icons on the Timeline menu to add/remove tracks.",380);
        }
        if(whichClick == 4)
        {
          layers.add(new PopText(ACTIVELAYER,12,"Enter new vScale:",0));
          ACTIVELAYER++;
        }
      lastClick = millis();      
    }else{
      dragActive = false;
      Arrays.fill(resizeActive,false);
    }
  }
}
